/**
 * Copyright 2007-2008 Vidar Svansson
 *
 * This file is part of JAdapter.
 *
 * JAdapter is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * JAdapter is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with JAdapter.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.jadapter;

import org.jadapter.adapters.AutoConfigAdapter;
import org.jadapter.adapters.Adapters;
import org.jadapter.exceptions.AdaptationException;
import org.jadapter.registry.TransformerRegistry;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;

/**
 * An "transformer adapter", <em>i.e.</em> an adapter that adapts
 * classes that do not use the framework
 * to the {@link org.jadapter.Transformer} interface.
 * <br></br><br></br>
 * This class uses reflection to determine instantiate the adapter
 * and appropriate method to use as the {@link org.jadapter.Transformer}.
 * This resolution requires type information, which can be injected.
 * <br></br><br></br>
 * The resolution only takes place the first time the adapter is used.
 * The {@link #resolved()} method can be used to check if a reflection resolution
 * has been instantiated for this adapter.
 *
 * @author Vidar Svansson
 * @param <I> The type provided by this object.
 * @param <C> The type this adapter can adapt.
 * @since 0.1
 */
public class JAdapter<I, C> extends AutoConfigAdapter<I, C> {


    /* The internal adapter, is created on demand or can be injected */
    private transient Transformer<I, C> transformer;

    /* Reference to the registry that created the adapter */
    private transient TransformerRegistry transformers;


    /**
     * Default no-arg constructor.
     * Type information and the adapter class will have to be injected
     * after initialization.
     *
     * @deprecated
     */
    public JAdapter() {
        super(null);
    }

    /**
     * @deprecated never needed
     */
    public JAdapter(TransformerRegistry transformers) {
        super(null);
        this.transformers = transformers;
    }

    /**
     * Create an adapter with all dependencies.
     */
    public JAdapter(Class<?> adapter, Class<I> to, Class<C> from) {
        super(adapter);
        to(to);
        from(from);
    }

    /**
     * Create an adapter with all dependencies.
     *
     * @deprecated never needed
     */
    public JAdapter(Class<?> adapter, Class<I> to, Class<C> from,
                    TransformerRegistry transformers) {
        super(adapter);
        to(to);
        from(from);
        this.transformers = transformers;
    }

    /**
     * Create an adapter without type information.
     *
     * @param adapter The adapter class ("the factory").
     */
    public JAdapter(Class<?> adapter) {
        super(adapter);

    }

    /**
     * @param adapter
     * @deprecated use @link org.jadapter.adapters.TransformerAdapter
     */
    public JAdapter(Transformer<I, C> adapter) {
        super(null);
        this.transformer = adapter;

    }


    /**
     * @deprecated never needed.
     *             Use @link org.jadapter.adapters.TransformerAdapter
     */
    public JAdapter(Transformer<I, C> adapter, TransformerRegistry transformers) {
        super(null);
        this.transformer = adapter;
        this.transformers = transformers;
    }

    /**
     * @deprecated never needed
     */
    public JAdapter(Class<?> adapterClass,
                    TransformerRegistry transformers) {
        super(adapterClass);
        this.transformers = transformers;
    }


    Query<Callable<I, C>> invokers = new Query<Callable<I, C>>(
            new SingleArgInjectionInvoker(),
            new SingleArgInvoker(),
            new NoArgInvoker(),
            new VarArgInvoker()
    );


    /**
     * The implementation of the Adapter interface using reflection.
     * <br></br><br></br>
     * The algorithm to resolve the transformation is roughly as follows:
     * <ol>
     * <li>If context is of type I, return it without any processing</li>
     * <li>If a transformer instance is available, delegate the processing to it and return the result</li>
     * <li>Use reflection to instantiate the factory class and inject the context and retrieve a result of type I</li>
     * </ol>
     */
    @Override
    public I doTransform(C context, Type type) {

        Object instance = null;

        if (transformer != null) {
            instance = transformer.transform(context);
            @SuppressWarnings("unchecked")
            I result = (I) instance;
            return result;
        }


        try {
            return invokers.xor(NoSuchMethodException.class).call(context);
        } catch (SecurityException e) {
            throw new AdaptationException("Constructor should be public", e);
        }
        catch (Exception e) {  // TODO remove
            throw new RuntimeException(e);
        }

    }


    private Class<?> getFromClass() {
        Type from = from();
        if (from instanceof Class)
            return (Class) from;
        throw new IllegalArgumentException("C must be a Class");
    }

    /**
     * @deprecated use the constructor to set the factory.
     */
    @Override
    public void setFactory(Class<?> factory) {
        super.setFactory(factory);

    }

    private final class SingleArgInjectionInvoker implements Callable<I, C> {
        public I call(C context) throws Exception {

            Constructor<?> constructor = getFactory().getConstructor(getFromClass(), TransformerRegistry.class);
            Object instance = constructor.newInstance(context, transformers);

            if (Adapters.isInstance(instance, to())) {
                transformer = new SingleArgInjectionTransformer((Constructor<I>) constructor);
                return (I) instance;
            }

            throw new AdaptationException("Adapter is of incorrect type");
        }
    }


    private final class SingleArgInvoker implements Callable<I, C> {
        public I call(C context) throws Exception {

            Constructor<?> constructor = getFactory().getConstructor(getFromClass());
            Object instance = constructor.newInstance(context);

            if (Adapters.isInstance(instance, to())) {
                transformer = new SingleArgTransformer(constructor);
                return (I) instance;
            }

            throw new AdaptationException("Adapter is of incorrect type");
        }
    }

    private final class NoArgInvoker implements Callable<I, C> {
        public I call(C context) throws Exception {
            Constructor<?> constructor = getFactory().getConstructor();
            Object instance = constructor.newInstance();

            if (instance instanceof Transformer) {
                Transformer<I, C> t = (Transformer<I, C>) instance;

                I result = t.transform(context);

                if (Adapters.isInstance(result, to())) {
                    transformer = t;
                    return result;
                }

                throw new AdaptationException("Transformer is not suitable for this adapter. "
                        + "Expected: " + from() + ", got " + result.getClass());
            }

            if (Adapters.isInstance(instance, to())) {
                I i = (I) instance;
                Class<?>[] types;
                for (Method m : getFactory().getDeclaredMethods()) {
                    types = m.getParameterTypes();
                    if (types.length == 1 && types[0].isInstance(context)) {

                        m.invoke(i, context);

                        transformer = new InjectionMethodTransformer(getFactory().getConstructor(), m);
                        return i;
                    }
                }
                throw new AdaptationException("No suitable injection method");
            }
            Class<?>[] types;
            Object result;

            for (Method m : instance.getClass().getDeclaredMethods()) {
                types = m.getParameterTypes();
                if (types.length == 1 && types[0].isInstance(context)
                        && Adapters.isInstance(m.getReturnType(), to())) {

                    result = m.invoke(instance, context);
                    if (Adapters.isInstance(result, to())) {
                        transformer = new FactoryMethodTransformer(getFactory().getConstructor(), m);
                        return (I) result;
                    }
                }
            }

            throw new AdaptationException("No suitable factory method found");
        }
    }

    private final class VarArgInvoker implements Callable<I, C> {
        public I call(C context) throws Exception {
            for (Constructor<?> constructor : getFactory().getConstructors()) {
                Class<?>[] types = constructor.getParameterTypes();
                if (constructor.isVarArgs() && getFromClass().isAssignableFrom(types[0])) {
                    if (types.length == 2) {
                        Object instance = constructor.newInstance(context, null);
                        if (Adapters.isInstance(instance, to())) {
                            transformer = new VarArgTransformer((Constructor<I>) constructor);
                            return (I) instance;
                        }

                        throw new AdaptationException("Adapter is of incorrect type");
                    }
                }
            }
            throw new AdaptationException("Unable to find any suitalbe constructor");
        }
    }


    class FactoryMethodTransformer implements Transformer<I, C> {
        Method method;
        Constructor constructor;

        FactoryMethodTransformer(Constructor constructor, Method method) {
            this.method = method;
            this.constructor = constructor;
        }

        public I transform(C context) {
            try {
                Object instance = constructor.newInstance();
                Object o = method.invoke(instance, context);
                return (I) o;
            } catch (IllegalArgumentException e) {
                throw new AdaptationException("failed to create instance", e);
            } catch (InstantiationException e) {
                throw new AdaptationException("failed to create instance", e);
            } catch (IllegalAccessException e) {
                throw new AdaptationException("failed to create instance", e);
            } catch (InvocationTargetException e) {
                throw new AdaptationException("failed to create instance", e);
            }
        }

    }

    protected static class InjectionMethodTransformer<I,C> implements Transformer<I, C> {
        Method method;
        Constructor constructor;

        InjectionMethodTransformer(Constructor constructor, Method method) {
            this.method = method;
            this.constructor = constructor;
        }

        public I transform(C context) {
            try {
                Object instance = constructor.newInstance();
                Object o = method.invoke(instance, context);
                return (I) instance;
            } catch (IllegalArgumentException e) {
                throw new AdaptationException("failed to create instance", e);
            } catch (InstantiationException e) {
                throw new AdaptationException("failed to create instance", e);
            } catch (IllegalAccessException e) {
                throw new AdaptationException("failed to create instance", e);
            } catch (InvocationTargetException e) {
                throw new AdaptationException("failed to create instance", e);
            }
        }

    }


    public static class SingleArgTransformer<I,C> implements Transformer<I, C> {
        Constructor<I> constructor;

        public SingleArgTransformer(Constructor<I> constructor) {
            this.constructor = constructor;
        }

        public I transform(C item) {
            try {
                return constructor.newInstance(item);
            } catch (Exception e) {
                throw new AdaptationException("SingleArgTransformer failed to create instance", e);
            }
        }
    }

    class SingleArgInjectionTransformer implements Transformer<I, C> {
        Constructor<I> constructor;

        public SingleArgInjectionTransformer(Constructor<I> constructor) {
            this.constructor = constructor;
        }

        public I transform(C item) {
            try {
                return constructor.newInstance(item, transformers);
            } catch (Exception e) {
                throw new AdaptationException("SingleArgTransformer failed to create instance", e);
            }
        }
    }

    class VarArgTransformer implements Transformer<I, C> {
        Constructor<I> constructor;

        public VarArgTransformer(Constructor<I> constructor) {
            this.constructor = constructor;
        }

        public I transform(C item) {
            try {
                return constructor.newInstance(item, null);
            } catch (Exception e) {
                throw new AdaptationException("SingleArgTransformer failed to create instance", e);
            }
        }
    }


    public static <I, C> JAdapter<I, C> NEW(Class<?> adapter, Class<I> provides, Class<C> context) {
        return new JAdapter<I, C>(adapter, provides, context);
    }


    public Transformer<I, C> getTransformer() {
        return transformer;
    }

    public void setTransformer(Transformer<I, C> transformer) {
        this.transformer = transformer;
    }

    public void setFrom(Class<?> from) {
        from(from);
    }

    public void setTo(Class<?> to) {
        to(to);
    }



    /**
     * @return true if an internal adapter has been initialized for this
     *         instance, <em>i.e. after the first call to it or it has been injected</em>.
     * @deprecated Changing to fail-fast in version 0.5. Which makes this obsolete.
     */
    public boolean resolved() {
        return transformer != null;
    }
}
